Skip to content

Add forward LSE output to FlyDSL flash attention kernels#844

Open
amd-wsung102 wants to merge 1 commit into
ROCm:mainfrom
amd-wsung102:forward_lse
Open

Add forward LSE output to FlyDSL flash attention kernels#844
amd-wsung102 wants to merge 1 commit into
ROCm:mainfrom
amd-wsung102:forward_lse

Conversation

@amd-wsung102

@amd-wsung102 amd-wsung102 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Motivation

This PR is motivated by the task described here https://amd-hub.atlassian.net/jira/software/c/projects/AIMODELS/boards/2992?assignee=712020%3Acedccbdf-bb1e-4e04-8c3b-edcdd7744eda&selectedIssue=AIMODELS-983, which prepares for a MSLK integration.

Summary

Extends the FlyDSL flash attention forward kernels to optionally emit the per-row log-sum-exp (LSE) alongside the attention output. LSE is the normalization statistic the backward pass needs to recompute the softmax probabilities P without re-running the online softmax, so this is the enablement step for a pure-FlyDSL forward+backward attention path.

Opt-in via return_lse=True; default behavior is unchanged.

Changes

  • flash_attn_interface.py: add return_lse to flydsl_flash_attn_func; thread it through every build-cache helper; allocate fp32 [B, num_heads, Sq] and return (out, lse) when enabled. Cache is keyed on return_lse so the existing no-LSE kernels are never perturbed.
  • flash_attn_generic.py: return_lse build flag; LSE tensor in the kernel/launch/compile signatures; store sm_scale*m_raw + ln(l) on both the normal store and the zeroed (fully-masked) tile.
  • flash_attn_gfx950.py: same for the dualwave kernel (dense store m_row*ln2 + ln(l_row)), plus the split-K combine kernel finalizing m_max*ln2 + ln(den) from the per-split (m, l). One writer per row via half-wave + OOB-sentinel guards.
  • tests/kernels/test_flash_attn_lse.py: numerical validation vs a float32 PyTorch reference.
  • docs/flash_attn_lse_contract.md: the A1↔A3 interface contract.

Output tensor

  • dtype float32, shape [B, num_heads, Sq] (varlen: [B, num_heads, max_seqlen_q], padded rows undefined).

Supported / unsupported paths

Path LSE
dense (gfx950 dualwave / generic) yes
dense split-K (num_kv_splits > 1) yes
varlen (packed cu_seqlens) yes
GQA / MQA, causal, cross-attn (Sq≠Skv) yes
fp8 no — raises NotImplementedError
paged KV (block_table / seqlen_k) no — raises NotImplementedError

fp8 and paged KV are decode-oriented and out of scope for the training backward path; they fail loudly rather than silently returning wrong LSE.

Validation

tests/kernels/test_flash_attn_lse.py33 cases pass on gfx950, covering dense MHA/GQA/MQA (D=64/128, bf16/f16), split-K, varlen, cross-attention, and fully-masked (-inf) rows. LSE matches the fp32 reference within ~8e-3 (bf16) / ~4e-3 (f16); fp32-accumulating paths (varlen light, D=64) match to ~1e-6.

Performance — LSE store overhead

The LSE store is one fp32 write per query row (single lane per row), negligible next to the O(S²·D) attention compute. Measured on gfx950 (MI350), bf16, H=16, D=128, min-of-trials:

Shape (B, S, H, D) causal no-LSE kernel (µs) +LSE kernel (µs) overhead
(4, 2048, 16, 128) yes 99.1 100.3 +1.3%
(2, 4096, 16, 128) yes 157.4 157.1 ~0%
(8, 1024, 16, 128) no 99.2 99.5 +0.3%
Pure kernel-side overhead is within run-to-run noise (≈0–1.3%). Through the full public API the delta is ≈3%, dominated by the extra torch.empty allocation of the LSE output tensor, not the GPU store.

Performance — FlyDSL forward vs CK

FlyDSL forward throughput vs the CK backend (aiter mha_fwd), confirming the pure-FlyDSL forward path is competitive. gfx950 (MI350), bf16, host-side end-to-end (CUDA events), mean ± stddev over 3 runs. aiter_asm (hand-tuned fmha_v3) shown for reference. Baseline FlyDSL forward (LSE adds ≤1.3%, see above).

Shape (B, S, H, Hkv, D) mask FlyDSL µs FlyDSL TFLOPS CK µs CK TFLOPS Fly/CK asm µs asm TFLOPS
(2, 2048, 16, 16, 128) causal 54.0 ± 0.2 636 ± 2 86.1 ± 1.2 399 ± 6 1.59× 63.2 ± 0.5 544 ± 5
(2, 4096, 16, 16, 128) causal 162.6 ± 1.2 846 ± 6 190.2 ± 3.1 723 ± 12 1.17× 124.1 ± 0.2 1108 ± 2
(1, 8192, 16, 16, 128) causal 267.4 ± 1.8 1028 ± 7 329.9 ± 2.6 833 ± 7 1.23× 226.8 ± 0.2 1212 ± 1
(2, 2048, 16, 2, 128) causal GQA 54.2 ± 0.8 634 ± 9 86.2 ± 0.8 399 ± 4 1.59× 63.3 ± 0.2 544 ± 2
(4, 2048, 8, 8, 64) causal 42.4 ± 0.3 405 ± 3 62.4 ± 0.4 276 ± 2 1.47× n/a n/a
(8, 1024, 16, 16, 128) non-causal 71.6 ± 0.2 960 ± 2 92.1 ± 2.4 747 ± 19 1.29× 73.7 ± 0.2 933 ± 1
(2, 4096, 16, 16, 128) non-causal 212.6 ± 0.1 1293 ± 0 283.1 ± 1.0 971 ± 4 1.33× 223.7 ± 0.2 1229 ± 1

FlyDSL is faster than CK on every shape measured (1.17×–1.59×), and matches or beats the hand-tuned aiter_asm path on non-causal, GQA, short-sequence, and D=64 shapes (asm remains ahead on long causal). Correctness matches CK: FlyDSL max-err vs the shared reference is on par (Fly/CK max-err ratio = 1.00×; e.g. 3.9e-03 bf16 causal, 2.4e-04 non-causal), all runs PASS the harness gate.

Compatibility

Fully backwards compatible — return_lse defaults to False and returns a bare output tensor; LSE build variants are cached separately.

Test plan

  • pytest tests/kernels/test_flash_attn_lse.py on gfx950 (33 passed)
  • CI on target architectures

@coderfeli
coderfeli requested a review from yanguahe July 14, 2026 09:24
@yanguahe

Copy link
Copy Markdown
Contributor

Thanks for adding the forward LSE support. A couple of requests before this can be merged:

  1. The FMHA kernel code had a fairly large refactor last week, so please rebase/update this PR and fix the resulting merge conflicts against the current code structure.

  2. Please fold the LSE coverage from tests/kernels/test_flash_attn_lse.py into the existing FMHA forward test file, tests/kernels/test_flash_attn_fwd.py, instead of adding a separate new test file.

exe(q_flat, k_flat, v_flat, o_flat, B, Sq, **kwargs)
else:
kwargs: dict = dict(stream=launch_stream)
kwargs: dict = dict(lse=lse, stream=launch_stream)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker: this passes lse to every dense launcher, including the fp8 path when return_lse=False. The fp8 launcher does not accept an lse keyword, so fp8 forward will fail before launch with an unexpected keyword argument. Please only add lse to kwargs for non-fp8 launchers, or update the fp8 launcher to accept and ignore lse=None.

cq = cu_seqlens_q if cu_seqlens_q is not None else Out
ck = cu_seqlens_kv if cu_seqlens_kv is not None else Out
bt = block_table if block_table is not None else Out
ls = lse if lse is not None else Out

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker: when this builder is created with return_lse=True but the caller omits lse, this falls back to Out. Since RETURN_LSE stores fp32 LSE, that can overwrite the output buffer or write through an incompatible layout. Please raise ValueError when RETURN_LSE and lse is None instead of using Out as the fallback.

block_table_stride = 0
# LSE is only written under const_expr(RETURN_LSE); O placeholder otherwise.
if lse is None:
lse = O

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker: same issue here. If return_lse=True and the caller forgets to pass lse, this falls back to O, so the kernel can write fp32 LSE into the output buffer. Please fail fast with ValueError when RETURN_LSE and lse is None.

@yanguahe

Copy link
Copy Markdown
Contributor

One style request: for code comments, except function-level comments/docstrings, please keep comments concise, ideally 1-2 lines. Several added inline comments are longer than needed and would be easier to maintain if shortened.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants